﻿//-----------------------------------------------------------------------
// <copyright file="DataBag.cs" company="Nathan Miller">
// Copyright (c) Nathan Miller.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </copyright>
//-----------------------------------------------------------------------
namespace TileTool.DataBag
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using TileTool.Common.Math;
    using TileTool.DataBag.Generic;

    /// <summary>
    /// Data bag class which is a dictionary with a key of a string name
    /// and a value of a IDataBagValue.
    /// </summary>
    public class DataBag : IDataBag
    {
        #region Fields - Private

        /// <summary>
        /// Dictionary of values we contain.
        /// </summary>
        private IDictionary<string, IDataBagValue> values = new Dictionary<string, IDataBagValue>();

        #endregion

        #region Events

        /// <summary>
        /// Event triggered when a data bag value changes.
        /// 
        /// First parameter is the data bag that contains the value.
        /// Second parameter is the value that changed.
        /// </summary>
        public event Action<IDataBag, IDataBagValue> Changed;

        #endregion

        #region Properties - Public

        /// <summary>
        /// Gets a collection containing the keys of the dictionary.
        /// </summary>
        public ICollection<string> Keys
        {
            get
            {
                return this.values.Keys;
            }
        }

        /// <summary>
        /// Gets a collection containing the values of the dictionary.
        /// </summary>
        public ICollection<IDataBagValue> Values
        {
            get
            {
                return this.values.Values;
            }
        }

        /// <summary>
        /// Gets the number of elements
        /// </summary>
        public int Count
        {
            get
            {
                return this.values.Count;
            }
        }

        /// <summary>
        /// Gets or sets the element with the specified key.
        /// </summary>
        /// <param name="key">Key to get.</param>
        /// <returns>Returns the value associated with key.</returns>
        public IDataBagValue this[string key]
        {
            get
            {
                return this.values[key];
            }

            private set
            {
                this.values[key] = value;
            }
        }

        #endregion

        #region Methods - Public static

        /// <summary>
        /// Build a data bag from the passed string dictionary.
        /// 
        /// The key to the dictionary is the name of the data bag value.
        /// The value in the dictionary is an array of two strings. The first string is the type
        /// of data bag value (DataBagValue.ToString()) and the second string is the data bag
        /// value in string form.
        /// </summary>
        /// <param name="dict">Dictionary to build from</param>
        /// <returns>Returns the data bag that was build from the dictionary. In the case of an error
        /// an empty data bag will be returned.</returns>
        public static DataBag BuildFromStringDictionary(IDictionary<string, string[]> dict)
        {
            if (dict == null)
            {
                throw new ArgumentNullException("dict", "Dictionary must be non-null.");
            }

            DataBag res = new DataBag();

            foreach (KeyValuePair<string, string[]> item in dict)
            {
                string name = item.Key;
                if (string.IsNullOrEmpty(name))
                {
                    return null;
                }

                string[] value = item.Value;
                if (value == null || value.Length != 2)
                {
                    return null;
                }

                CreateDataBagValue(res, name, value);
            }

            return res;
        }

        #endregion

        #region Methods - Public

        /// <summary>
        /// Create a value of the passed type with the provided name and default value.
        /// </summary>
        /// <typeparam name="T">Type of value to create.</typeparam>
        /// <param name="name">Name of the value to create.</param>
        /// <param name="defaultValue">Default value for the value.</param>
        /// <returns>Returns the new data bag value.</returns>
        public DataBagValue<T> CreateValue<T>(string name, T defaultValue = default(T))
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentNullException("name", "Name must be non-null.");
            }

            DataBagValue<T> value = new DataBagValue<T>(defaultValue);

            value.Parent = this;

            this[name] = value;

            return value;
        }

        /// <summary>
        /// Get a value of the passed type with the provided name.
        /// 
        /// If we don't find it, will create a new value.
        /// </summary>
        /// <typeparam name="T">Type of value to get.</typeparam>
        /// <param name="name">Name of the value.</param>
        /// <param name="defaultValue">Default value for the value.</param>
        /// <returns>Returns the data bag value that was found or the value that was created.</returns>
        public DataBagValue<T> GetOrCreateValue<T>(string name, T defaultValue = default(T))
        {
            IDataBagValue res = null;

            if (this.TryGetValue(name, out res))
            {
                DataBagValue<T> typesRes = res as DataBagValue<T>;
                if (typesRes == null)
                {
                    return this.CreateValue<T>(name, defaultValue);
                }

                return typesRes;
            }

            return this.CreateValue<T>(name, defaultValue);
        }

        /// <summary>
        /// Clone this data bag by creating a deep copy.
        /// </summary>
        /// <returns>Returns a copy of the data bag.</returns>
        public IDataBag Clone()
        {
            DataBag cloneBag = new DataBag();

            foreach (KeyValuePair<string, IDataBagValue> item in this)
            {
                IDataBagValue cloneValue = item.Value.Clone();
                DataBagValueBase cloneBase = cloneValue as DataBagValueBase;

                if (cloneBase == null)
                {
                    throw new InvalidOperationException("Clone was not of the DataBagValueBase type.");
                }

                cloneBase.Parent = this;

                cloneBag[item.Key] = cloneValue;
            }

            return cloneBag;
        }

        /// <summary>
        /// Determines if the dictionary contains the key.
        /// </summary>
        /// <param name="key">Key to locate.</param>
        /// <returns>Returns true if the dictionary contains the key.</returns>
        public bool ContainsKey(string key)
        {
            return this.values.ContainsKey(key);
        }

        /// <summary>
        /// Removes the element with the specified key from the bag.
        /// </summary>
        /// <param name="key">The key of the element to remove.</param>
        /// <returns>true if the element is successfully removed; otherwise, false.</returns>
        public bool Remove(string key)
        {
            bool res = this.values.Remove(key);

            return res;
        }

        /// <summary>
        /// Gets the value associated with the specified key.
        /// </summary>
        /// <param name="key">The key whose value to get.</param>
        /// <param name="value">When this method returns, the value associated with the specified key,
        /// if the key is found; otherwise, the default value for the type of the value parameter.
        /// This parameter is passed uninitialized.</param>
        /// <returns>true if the object is found.</returns>
        public bool TryGetValue(string key, out IDataBagValue value)
        {
            return this.values.TryGetValue(key, out value);
        }

        /// <summary>
        /// Gets the value associated with the specified key.
        /// </summary>
        /// <typeparam name="T">Type of value to get.</typeparam>
        /// <param name="key">The key whose value to get.</param>
        /// <param name="value">When this method returns, the value associated with the specified key,
        /// if the key is found; otherwise, the default value for the type of the value parameter.
        /// This parameter is passed uninitialized.</param>
        /// <returns>true if the object is found.</returns>
        public bool TryGetValue<T>(string key, out IDataBagValue<T> value)
        {
            value = null;

            IDataBagValue tempValue;
            if (!this.values.TryGetValue(key, out tempValue))
            {
                return false;
            }

            value = tempValue as IDataBagValue<T>;
            if (value == null)
            {
                return false;
            }

            return true;
        }

        /// <summary>
        /// Convert the data bag to a dictionary which contains only strings.
        /// 
        /// The key to the dictionary is the name of the value.
        /// The value in the dictionary is an array of two strings. The first string is the type
        /// of data bag value and the second string is the data bag value in string form.
        /// </summary>
        /// <returns>Returns the databag as a dictionary of strings.</returns>
        public IDictionary<string, string[]> ToStringDictionary()
        {
            IEnumerable<string> keys = this.values.Keys;
            IDictionary<string, string[]> resDict = new Dictionary<string, string[]>();

            foreach (string key in keys)
            {
                IDataBagValue value = this.values[key];
                string valueString = value.ToString();
                string enumString = value.Type.ToString();

                resDict.Add(key, new string[2] { enumString, valueString });
            }

            return resDict;
        }

        /// <summary>
        /// Returns an enumerator that iterates through the collection.
        /// </summary>
        /// <returns>A iterator that can be used to iterate through the collection.</returns>
        public IEnumerator<KeyValuePair<string, IDataBagValue>> GetEnumerator()
        {
            return this.values.GetEnumerator();
        }

        /// <summary>
        /// Returns an enumerator that iterates through a collection.
        /// </summary>
        /// <returns>An enumerator object that can be used to iterate through the collection.</returns>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.values.GetEnumerator();
        }
        
        /// <summary>
        /// Clears the data bag.
        /// </summary>
        public void Clear()
        {
            this.values.Clear();
        }

        #endregion

        #region Methods - Internal

        /// <summary>
        /// Called when a data bag value changes.
        /// </summary>
        /// <param name="value">Value that changed.</param>
        internal void ValueChanged(IDataBagValue value)
        {
            if (value == null)
            {
                throw new ArgumentNullException("value", "Value must be non-null.");
            }

            Action<IDataBag, IDataBagValue> temp = this.Changed;
            if (temp == null)
            {
                return;
            }

            temp(this, value);
        }

        #endregion

        #region Methods - Private static

        /// <summary>
        /// Create a data bag value in the passed bag with the passed name from the type and
        /// value info.
        /// </summary>
        /// <param name="bag">Bag to create the value in.</param>
        /// <param name="name">Name of the data bag value.</param>
        /// <param name="valueInfo">Type and value information. valueInfo[0] is the value type
        /// and valueInfo[1] is the value as a string.</param>
        /// <returns>Returns true if the value was created.</returns>
        private static bool CreateDataBagValue(DataBag bag, string name, string[] valueInfo)
        {
            DataBagValueType valueType = DataBagValueType.None;
            try
            {
                valueType = (DataBagValueType)Enum.Parse(typeof(DataBagValueType), valueInfo[0]);
            }
            catch
            {
                return false;
            }

            if (valueType == DataBagValueType.None)
            {
                return false;
            }

            bool success = false;
            string value = valueInfo[1];

            switch (valueType)
            {
                case DataBagValueType.Int:
                    {
                        int res;
                        if (int.TryParse(value, out res))
                        {
                            bag.CreateValue<int>(name, res);
                            success = true;
                        }
                    }

                    break;
                case DataBagValueType.UInt:
                    {
                        uint res;
                        if (uint.TryParse(value, out res))
                        {
                            bag.CreateValue<uint>(name, res);
                            success = true;
                        }
                    }

                    break;
                case DataBagValueType.Long:
                    {
                        long res;
                        if (long.TryParse(value, out res))
                        {
                            bag.CreateValue<long>(name, res);
                            success = true;
                        }
                    }

                    break;
                case DataBagValueType.ULong:
                    {
                        ulong res;
                        if (ulong.TryParse(value, out res))
                        {
                            bag.CreateValue<ulong>(name, res);
                            success = true;
                        }
                    }

                    break;
                case DataBagValueType.Float:
                    {
                        float res;
                        if (float.TryParse(value, out res))
                        {
                            bag.CreateValue<float>(name, res);
                            success = true;
                        }
                    }

                    break;
                case DataBagValueType.String:
                    bag.CreateValue<string>(name, value);
                    break;
                case DataBagValueType.ColorRGBA:
                    {
                        ColorRGBA res;
                        if (ColorRGBA.TryParse(value, out res))
                        {
                            bag.CreateValue<ColorRGBA>(name, res);
                            success = true;
                        }
                    }

                    break;
                case DataBagValueType.Vector2D:
                    {
                        Vector2D res;
                        if (Vector2D.TryParse(value, out res))
                        {
                            bag.CreateValue<Vector2D>(name, res);
                            success = true;
                        }
                    }

                    break;
                case DataBagValueType.Matrix2D:
                   {
                        Matrix2D res;
                        if (Matrix2D.TryParse(value, out res))
                        {
                            bag.CreateValue<Matrix2D>(name, res);
                            success = true;
                        }
                    }

                    break;
                case DataBagValueType.Size2D:
                    {
                        Size2D res;
                        if (Size2D.TryParse(value, out res))
                        {
                            bag.CreateValue<Size2D>(name, res);
                            success = true;
                        }
                    }

                    break;
            }

            return success;
        }

        #endregion
    }
}
